package com.giggle.mybatis.Interceptor;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.giggle.mybatis.algorithm.HashModTable;
import com.giggle.mybatis.algorithm.HashingTable;
import com.giggle.mybatis.annotation.ShardingTable;
import org.apache.commons.collections.map.LinkedMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.persistence.Table;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.util.*;

/**
 * @author guozichen
 * @ClassName:
 * @Description: (分库分表)
 * @date 2020/8/31 15:28
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Component
public class ShardingInterceptor implements Interceptor {
    private static Logger logger = LoggerFactory.getLogger(ShardingInterceptor.class);
    /**hash取模方式分表*/
    private static final int HASH_MOD = 1;
    /**一致性hash分表*/
    private static final int HASH_HASHING = 2;

    public ShardingInterceptor() {
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        BoundSql boundSql = null;
        MetaObject metaObject = null;
        MappedStatement mappedStatement = null;
        Object target = invocation.getTarget();
        if (target instanceof RoutingStatementHandler) {
            RoutingStatementHandler routingStatementHandler = (RoutingStatementHandler)target;
            metaObject = SystemMetaObject.forObject(routingStatementHandler);
            StatementHandler statementHandler = (StatementHandler)metaObject.getValue("delegate");
            metaObject = SystemMetaObject.forObject(statementHandler);
            mappedStatement = (MappedStatement)metaObject.getValue("mappedStatement");
            boundSql = statementHandler.getBoundSql();
            /**通过预编译语句获取数据库实体和数据库表*/
            Class<?> mapperClass = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
            /**获取对应实体的class*/
            Class<?> eoClass = this.getEoClass(mapperClass);
            /**判断是否是分表*/
            if (eoClass.isAnnotationPresent(ShardingTable.class) && eoClass.isAnnotationPresent(Table.class)) {
                String maintable = ((Table)eoClass.getAnnotation(Table.class)).name();
                ShardingTable rdsSharding = (ShardingTable)eoClass.getAnnotation(ShardingTable.class);
                int algorithm = rdsSharding.algorithm();
                int tableTotal = rdsSharding.tableCount();
                String shardingProperty = rdsSharding.property();
                String tableNumParam = rdsSharding.tableNumParam();
                if (StringUtils.isBlank(shardingProperty)) {
                    logger.error("{} not find splitkey!", maintable);
                } else {
                    Configuration configuration = mappedStatement.getConfiguration();
                    String subTableName = null;
                    Object shardingValue = this.getShardingValue(boundSql, shardingProperty,configuration);
                    if (shardingValue != null) {
                        /**根据不同的模式获取对应的分表名*/
                        if (HASH_MOD == algorithm) {
                            subTableName = HashModTable.getSplitTableName(maintable, shardingValue, tableTotal);
                        } else {
                            subTableName = HashingTable.getSplitTableName(maintable, shardingValue, tableTotal);
                        }
                    } else {
                        Object tableNumber = this.getTableNumber(boundSql, tableNumParam);
                        if (tableNumber != null) {
                            subTableName = maintable + "_" + tableNumber.toString();
                        }
                    }

                    /**替换sql*/
                    if (StringUtils.isNotBlank(subTableName)) {
                        String sql = boundSql.getSql();
                        sql = sql.replaceAll(maintable, subTableName);
                        metaObject = SystemMetaObject.forObject(boundSql);
                        metaObject.setValue("sql", sql);
                    } else {
                        logger.error("Unable to obtain subTableName , exec canceled. caseby: {} splitKey's value is null or tableNumParam's value {} is null.", maintable, tableNumParam);
                    }
                }
            }
        }

        return invocation.proceed();
    }

    /**
     * 获取分表对应的属性
     * @param boundSql
     * @param shardingProperty
     * @param configuration
     * @return
     */
    private Object getShardingValue(BoundSql boundSql, String shardingProperty,Configuration configuration) {
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        Object parameterObject = boundSql.getParameterObject();
        Object value;
        if (boundSql.hasAdditionalParameter(shardingProperty)) {
            value = boundSql.getAdditionalParameter(shardingProperty);
        } else if (parameterObject == null) {
            //如果参数是null，不管属性名是什么，都会返回null。
            value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            //判断类型处理器是否有参数类型,如果参数是一个简单类型，或者是一个注册了typeHandler的对象类型，就会直接使用该参数作为返回值，和属性名无关。
            value = parameterObject;
        } else{
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            if(((MapperMethod.ParamMap)metaObject.getOriginalObject()).containsKey(shardingProperty)){
                value = metaObject.getValue(shardingProperty);
            }else{
                value = null;
            }
        }
        return value;
    }

    private Object getTableNumber(BoundSql boundSql, String tableNumParam) {
        Object parameterObj = boundSql.getParameterObject();
        Map<String, String> parameterMap = json2Map(JSON.toJSONString(parameterObj));
        return parameterMap.get(tableNumParam);
    }

    @Override
    public Object plugin(Object target) {
        return target instanceof StatementHandler ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 获取对应数据实体的class
     * @param eoMapper
     * @return
     */
    private Class<?> getEoClass(Class<?> eoMapper) {
        Class entityClass = this.getGenericClass(eoMapper);
        if (entityClass != null) {
            String eoName = entityClass.getPackage().getName() + "." + org.springframework.util.StringUtils.delete(entityClass.getSimpleName(), "Eo") + "ExtEo";

            try {
                Class extClass = Class.forName(eoName);
                entityClass = extClass;
            } catch (ClassNotFoundException var5) {
            }
        }

        return entityClass;
    }

    private Class<?> getGenericClass(Class<?> clazz) {
        Type t = clazz.getGenericSuperclass();
        if (t == null) {
            t = clazz.getGenericInterfaces()[0];
        }

        if (t instanceof ParameterizedType) {
            Type[] p = ((ParameterizedType)t).getActualTypeArguments();
            return (Class)p[0];
        } else {
            return null;
        }
    }

    private static Map<String, String> json2Map(String json) {
        LinkedMap map = new LinkedMap();
        JSONObject jsonObject = JSONObject.parseObject(json);
        return populate(jsonObject, map);
    }

    private static Map<String, String> populate(JSONObject jsonObject, Map<String, String> map) {
        Iterator iterator = jsonObject.entrySet().iterator();

        while(iterator.hasNext()) {
            String entryStr = String.valueOf(iterator.next());
            String key = entryStr.substring(0, entryStr.indexOf("="));
            Class clazz = jsonObject.get(key).getClass();
            if (clazz.equals(JSONObject.class)) {
                HashMap _map = new HashMap();
                map.putAll(populate(jsonObject.getJSONObject(key), _map));
            } else if (clazz.equals(JSONArray.class)) {
                ArrayList list = new ArrayList();
                map.putAll(populateArray(jsonObject.getJSONArray(key), list));
            } else {
                map.put(key, jsonObject.get(key).toString());
            }
        }

        return map;
    }

    private static Map populateArray(JSONArray jsonArray, List list) {
        HashMap map = new HashMap();

        for(int i = 0; i < jsonArray.size(); ++i) {
            Class clazz = jsonArray.get(i).getClass();
            if (clazz.equals(JSONArray.class)) {
                ArrayList _list = new ArrayList();
                list.add(_list);
                populateArray(jsonArray.getJSONArray(i), _list);
            } else if (clazz.equals(JSONObject.class)) {
                HashMap _map = new HashMap();
                list.add(_map);
                map.putAll(populate(jsonArray.getJSONObject(i), _map));
            } else {
                list.add(jsonArray.get(i));
            }
        }

        return map;
    }
}
